﻿using System.Collections.Generic;
using System.Linq;
using System.Windows;
using NUnit.Framework;
using Utility3D.Extensions;

namespace Utility3D
{
    [TestFixture]
    public class PointRefTest
    {
        [Test]
        public void TestBasicGlide()
        {
            var worldModel = GetWorldModel();
            var drawingSurface = worldModel.DrawingSurfaceChains.First();

            var point = new Point(10, 11);
            var pointRef = worldModel.Collections.AddPoint(drawingSurface, point);

            var glidedSurface = drawingSurface.Glide(10);

            var glidedRef = pointRef.GetOnSurface(glidedSurface);
            Assert.AreEqual(glidedRef.Value, point);

            pointRef.SetOnSurface(glidedSurface, new Point(20, 22));

            glidedSurface = drawingSurface.Glide(5);
            glidedRef = pointRef.GetOnSurface(glidedSurface);

            Assert.AreEqual(glidedRef.Value, new Point(15, 16.5));
        }

        private static WorldModel GetWorldModel()
        {
            var worldModel = new WorldModel();
            var surface = new DrawingSurface(worldModel, "Only View", new Plane(new Point3D(0, 0, 0), new Vector3D(100, 0, 0), new Vector3D(0, 100, 0)), 100, 100);
            worldModel.DrawingSurfaceChains.Add(surface);
            return worldModel;
        }

        [Test]
        public void TestTwoPointGlideLongGlideFirst()
        {
            var worldModel = GetWorldModel();
            var drawingSurface = worldModel.DrawingSurfaceChains.First();

            var a = worldModel.Collections.AddPoint(drawingSurface, new Point(20, 100));
            var b = worldModel.Collections.AddPoint(drawingSurface, new Point(30, 100));

            var glidedSurface = drawingSurface.Glide(20);
            b.SetOnSurface(glidedSurface, new Point(30, 80));

            glidedSurface = drawingSurface.Glide(10);
            a.SetOnSurface(glidedSurface, new Point(20, 110));

            glidedSurface = drawingSurface.Glide(5);
            var pointA = a.GetOnSurface(glidedSurface);
            var pointB = b.GetOnSurface(glidedSurface);

            Assert.AreEqual(105, pointA.Value.Y);
            Assert.AreEqual(95, pointB.Value.Y);

            glidedSurface = drawingSurface.Glide(15);
            pointA = a.GetOnSurface(glidedSurface);
            pointB = b.GetOnSurface(glidedSurface);

            Assert.AreEqual(110, pointA.Value.Y);
            Assert.AreEqual(85, pointB.Value.Y);
        }

        [Test]
        public void TestTwoPointGlide()
        {
            var worldModel = GetWorldModel();
            var drawingSurface = worldModel.DrawingSurfaceChains.First();

            var a = worldModel.Collections.AddPoint(drawingSurface, new Point(20, 100));
            var b = worldModel.Collections.AddPoint(drawingSurface, new Point(30, 100));

            var glidedSurface = drawingSurface.Glide(10);
            a.SetOnSurface(glidedSurface, new Point(20, 110));

            glidedSurface = drawingSurface.Glide(20);
            b.SetOnSurface(glidedSurface, new Point(30, 80));

            glidedSurface = drawingSurface.Glide(5);
            var pointA = a.GetOnSurface(glidedSurface);
            var pointB = b.GetOnSurface(glidedSurface);

            Assert.AreEqual(105, pointA.Value.Y);
            Assert.AreEqual(95, pointB.Value.Y);

            glidedSurface = drawingSurface.Glide(15);
            pointA = a.GetOnSurface(glidedSurface);
            pointB = b.GetOnSurface(glidedSurface);

            Assert.AreEqual(110, pointA.Value.Y);
            Assert.AreEqual(85, pointB.Value.Y);
        }

        //TODO: Add Test for dealing with adding vertexes on glided surfaces (after I define the behavior! not sure I like what it's doing now)
        //TODO: Add Test to make sure the PointRef is still working even if surfaces are removed
    }

    public interface IPointRef : IModelObject
    {
        Point? GetOnSurface(DrawingSurface drawingSurface);
        Point3D? GetPoint3DOnSurface(DrawingSurface drawingSurface);
        void SetOnSurface(DrawingSurface drawingSurface, Point point);
    }

    public class MultiSurfacePointRef : IPointRef
    {
        [IncludeInPersistence]
        private readonly Dictionary<DrawingSurface, Point> _surfacePoints = new Dictionary<DrawingSurface, Point>();

        public Point? GetOnSurface(DrawingSurface drawingSurface)
        {
            var previousReference = drawingSurface.ChainWithSelf(ds => ds.PrevSurface).FirstOrDefault(ds => _surfacePoints.ContainsKey(ds));
            var nextReference = drawingSurface.ChainWithSelf(ds => ds.NextSurface).FirstOrDefault( ds => _surfacePoints.ContainsKey(ds));
            previousReference = previousReference ?? nextReference;
            nextReference = nextReference ?? previousReference;
            if (previousReference == null || nextReference == null) return null;

            if (previousReference == nextReference) return _surfacePoints[previousReference];

            var previousPoint = _surfacePoints[previousReference];
            var nextPoint = _surfacePoints[nextReference];
            var glideRatio = (drawingSurface.DistanceFromBaseSurface - previousReference.DistanceFromBaseSurface)
                             /(nextReference.DistanceFromBaseSurface - previousReference.DistanceFromBaseSurface);
            var yOffset = glideRatio*(nextPoint.Y - previousPoint.Y);
            var xOffset = glideRatio*(nextPoint.X - previousPoint.X);
            return new Point(previousPoint.X + xOffset, previousPoint.Y + yOffset);
        }

        public Point3D? GetPoint3DOnSurface(DrawingSurface drawingSurface)
        {
            var point = GetOnSurface(drawingSurface);
            if (point == null) return null;

            var xVector = drawingSurface.Plane.VectorX*(point.Value.X/drawingSurface.Width);
            var yVector = drawingSurface.Plane.VectorY*(point.Value.Y/drawingSurface.Height);
            return drawingSurface.Plane.BasePoint + xVector + yVector;
        }

        public void SetOnSurface(DrawingSurface drawingSurface, Point point)
        {
            _surfacePoints[drawingSurface] = point;
        }

        public int Id { get; private set; }
    }
}